전체 QNA

React Context를 이중으로 사용하는 이유가 무엇인가요? (최적화 관련)

9/23/2023 작성

질문

강의 중 Context를 2개 생성하고 2중으로 배치해 각각의 Context에

state와 dispatch 함수들을 따로 제공하는데 왜 그렇게 하는 것인가요?

 

답변

안녕하세요 이정환입니다.

하나씩 천천히 정리해 보겠습니다!

우선 Context.Provider에 공급하는 value Props의 값이 변하면 Provider 하위의 컴포넌트들은 모두 리렌더 됩니다.

그런데 이 때 React.memo를 이용해 컴포넌트를 감싸 강화된(메모이제이션 된) 컴포넌트로 만들어주면 불 필요한 리렌더를 막을 수 있습니다. 따라서 React.memo로 감싸져 있는 컴포넌트들은 useContext로 구독하는 값이 변경되지 않는 이상 리렌더 되지 않습니다.

자 그러면 다음과 같은 코드가 있다고 가정하겠습니다.

COPY
// src/App.js

const StateContext = React.createContext();
const DispatchContext = React.createContext();

export default function App() {
  const [state, setState] = useState(0);

  const onIncrease = useCallback(() => {
    setState((state) => state + 1);
  }, []);

  const onDecrease = useCallback(() => {
    setState((state) => state - 1);
  }, []);

  return (
    <div className="App">
      <StateContext.Provider value={state}>
        <DispatchContext.Provider
          value={{
            onIncrease,
            onDecrease
          }}
        >
          <div>
            <ChildA />
            <ChildB />
          </div>
        </DispatchContext.Provider>
      </StateContext.Provider>
    </div>
  );
}

App 컴포넌트는 1개의 State를 가지고 있습니다. 그리고 이 State를 변경시키는 함수 onIncrease, onDecrease도 가지고 있습니다.

상태와 상태 변경함수를 Context를 이용해 Props Drilling 없이 컴포넌트 트리 전체에 공급해주기 위해 2개의 Context를 만들었습니다. 하나는 State를 공급할 StateContext, 하나는 상태변화 함수들을 공급할 DispatchContext입니다.

각각의 Context.Provider 컴포넌트에 value Props를 설정해 컴포넌트 트리에 값을 공급합니다. StateContext.Provider에는 state를 DispatchContext.Provider에게는 {onIncrease, onDecrease} 객체를 전달합니다.

다음으로 2개의 자식 컴포넌트를 만듭니다. ChildA, ChildB입니다.

COPY
const ChildA = React.memo(function () {
  const state = useContext(StateContext);
  useEffect(() => {
    console.log("A");
  });
  return <div>{state}</div>;
});

const ChildB = React.memo(function () {
  const { onIncrease, onDecrease } = useContext(DispatchContext);
  useEffect(() => {
    console.log("B");
  });
  return (
    <div>
      <button onClick={onDecrease}>-</button>
      <button onClick={onIncrease}>+</button>
    </div>
  );
});

ChildA는 State만 공급받아 화면에 렌더링하는 컴포넌트입니다.

반면 ChildB는 2개의 버튼이 있고 각 버튼을 클릭하면 DispatchContext로 부터 공급받은 상태변화 함수를(onIncrease, onDecrease) 호출하는 컴포넌트입니다.

정리하면 ChildA -> StateContext / ChildB -> DispatchContext의 값을 공급받습니다(구독이라고 표현해도 됩니다)

이 때 ChildA와 ChildB 모두 React.memo를 적용합니다. 이를 통해 우리가 기대하는 건 state의 값이 바뀌면 ChildA 컴포넌트만 리렌더되고, onIncrease, onDecrease 함수가 재 생성되면 ChildB 컴포넌트만 리렌더 되기를 바랍니다.

정말 그렇게 될까요? 실제로 코드샌드박스에서 실험 해 보았습니다.

아쉽게도 ChildA, ChildB 두 컴포넌트가 다 리렌더됩니다. 왜 이런걸까요? 분명히 React.memo로 감싸 강화한 컴포넌트는 자신이 구독하는 컨텍스트가 공급하는 값이 바뀌지 않으면 리렌더 되지 않는다고 했는데요?

문제는 App 컴포넌트에 있습니다. App 컴포넌트의 코드를 다시 보겠습니다.

COPY
const StateContext = React.createContext();
const DispatchContext = React.createContext();

export default function App() {
  const [state, setState] = useState(0);

  const onIncrease = useCallback(() => {
    setState((state) => state + 1);
  }, []);

  const onDecrease = useCallback(() => {
    setState((state) => state - 1);
  }, []);

  return (
    <div className="App">
      <StateContext.Provider value={state}>
        <DispatchContext.Provider
          value={{
            onIncrease,
            onDecrease
          }}
        >
          <div>
            <ChildA />
            <ChildB />
          </div>
        </DispatchContext.Provider>
      </StateContext.Provider>
    </div>
  );
}

DispatchContext의 value Props에 중점을 두고 살펴보겠습니다. 사실 이 객체는 App 컴포넌트의 state가 업데이트 되면 재 생성됩니다. App 컴포넌트가 리렌더되기 때문입니다. 컴포넌트는 함수이기 때문에 컴포넌트의 리렌더는 함수를 다시 호출하는 거라고 했습니다. 따라서 함수 내부에 선언한 객체도 다시 선언됩니다.

결국 state가 변경되면 DispatchContext.Provider에 공급하는 값이 바뀝니다. 그럼 React.memo를 ChildA, B에 적용한 이유가 사라졌습니다.

문제는 onIncrease, onDecrease를 감싸는 객체가 재 생성되서 발생합니다. 그럼 이 객체를 다시 생성하지 않도록 만들면 문제를 해결할 수 있습니다. 이럴 때 useMemo를 활용하면 됩니다.

COPY
const StateContext = React.createContext();
const DispatchContext = React.createContext();

export default function App() {
  const [state, setState] = useState(0);

  const onIncrease = useCallback(() => {
    setState((state) => state + 1);
  }, []);

  const onDecrease = useCallback(() => {
    setState((state) => state - 1);
  }, []);

  const memoizedDispatch = useMemo(() => ({ onIncrease, onDecrease }), []);

  return (
    <div className="App">
      <StateContext.Provider value={state}>
        <DispatchContext.Provider value={memoizedDispatch}>
          <div>
            <ChildA />
            <ChildB />
          </div>
        </DispatchContext.Provider>
      </StateContext.Provider>
    </div>
  );
}

변수 memoizedDispatch를 만들고 이 변수에 useMemo로 메모이제이션 한, 다시 생성되지 않도록 만든 onIncrease, onDecrease를 감싼 객체를 저장합니다. 이 memoizedDisaptch는 state의 값이 바뀌어도 재 생성되지 않습니다.

그 다음 memoizedDispatch를 DispatchContext.Proider의 value Props로 전달합니다. 이제 DispatchContext.Provider는 App 컴포넌트의 state가 바뀌어도 전달받는 Props가 달라지지 않습니다.

그럼 이제 버튼을 클릭해 state를 업데이트 해도 React.memo가 적용되고, StateContext를 구독하지 않는 ChildB 컴포넌트가 리렌더 되지 않는지 확인해 보겠습니다.

+버튼을 연타해도 ChildA 컴포넌트만 리렌더 되는것을 알 수 있습니다.

정리하자면 Context.Provider에 제공하는 value Props가 바뀌면 이 하위의 컴포넌트들은 모두 리렌더 된다.

이것을 막고 싶다면 리렌더를 방지하고 싶은 컴포넌트에 React.memo를 적용하면 된다.

그러나 함수, 객체 등을 value Props로 전달할 경우 Context에 데이터를 공급하는 컴포넌트가 리렌더 되면 함수나 객체도 다시 생성되기 때문에 리렌더 방지가 효과적으로 이루어지지 않는다.

이를 useMemo를 이용해 해결할 수 있다.

이렇게 정리해 보았습니다.